﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;

namespace SimplexEngine
{
    public class MD5ResourceLoader : IResourceLoader
    {
        public class MD5Parser
        {
            public Boolean LoadMD5(String filename)
            {
                try
                {
                    lines = System.IO.File.ReadAllLines(filename);
                    MoveToTop();
                }
                catch
                {
                    return false;
                }

                return true;
            }

            String[] lines;
            Int32 current_line = -1;
            Int32 current_position = 0;

            public void MoveToTop()
            {
                current_line = -1;
                MoveToNextLine();
            }

            public Boolean MoveToNextLine()
            {
                ++current_line;
                current_position = 0;

                return current_line < lines.Count();
            }

            public Boolean SeekToNextKey(String key)
            {
                while (current_line < lines.Count() && lines[current_line].IndexOf(key, current_position) < 0)
                    MoveToNextLine();

                if (current_line == lines.Count())
                    return false;

                current_position = lines[current_line].IndexOf(key, current_position) + key.Length;

                return true;
            }

            public Boolean Goto(Char[] keys)
            {
                while (current_line < lines.Count() && ((lines[current_line].IndexOfAny(keys, current_position) < 0 || (lines[current_line].IndexOfAny(keys, current_position) >= 0 && lines[current_line].IndexOfAny(keys, current_position) > lines[current_line].IndexOf("/", current_position) && lines[current_line].IndexOf("/", current_position) >= 0))))
                    MoveToNextLine();

                if (current_line == lines.Count())
                    return false;

                current_position = lines[current_line].IndexOfAny(keys, current_position);

                return true;
            }

            protected String ReadNumberString(String search_string)
            {
                if (!Goto(search_string.ToCharArray()))
                    return "0";
                String result = "";
                while (current_position < lines[current_line].Length && search_string.IndexOf(lines[current_line][current_position]) != -1)
                {
                    result += lines[current_line][current_position];
                    ++current_position;
                }
                return result;
            }

            public int ReadInt()
            {
                return int.Parse(ReadNumberString("0123456789-"));
            }

            public float ReadFloat()
            {
                return float.Parse(ReadNumberString("0123456789-."));
            }

            public String ReadString()
            {
                String result = "";

                SeekToNextKey("\"");

                while (current_position < lines[current_line].Length && lines[current_line][current_position] != '"')
                {
                    result += lines[current_line][current_position];
                    ++current_position;
                }

                return result;
            }

            public Vector2 ReadVector2()
            {
                SeekToNextKey("(");
                Vector2 vector = new Vector2(ReadFloat(), ReadFloat());
                SeekToNextKey(")");
                return vector;
            }

            public Vector3 ReadVector3()
            {
                SeekToNextKey("(");
                Vector3 vector = new Vector3(ReadFloat(), ReadFloat(), ReadFloat());
                SeekToNextKey(")");
                return vector;
            }
        }

        public class MD5Joint
        {
            public String Name = "";
            public MD5Joint Parent = null;
            public Vector3 Translation;
            public Quaternion Rotation;
        }

        public class MD5Hierarchy
        {
            public String Name = "";
            public int Parent = -1;
            public int Flags = 0;
            public int StartIndex = 0;
        }

        public class MD5Vertex
        {
            public Vector2 UV;
            public int WeightStartIndex = 0;
            public int WeightCount = 0;
        }

        public class MD5Triangle
        {
            public int[] VertexIndex = new int[3];
        }

        public class MD5Weight
        {
            public int JointIndex = 0;
            public float Bias = 0.0f;
            public Vector3 Position;
        }

        public class MD5MeshPart
        {
            public List<MD5Weight> Weights = new List<MD5Weight>();
            public List<MD5Triangle> Triangles = new List<MD5Triangle>();
            public List<MD5Vertex> Vertices = new List<MD5Vertex>();
        }

        public class MD5Frame
        {
            List<MD5Joint> Joints = new List<MD5Joint>();
        }

        public class MD5Animation
        {
            List<MD5Joint> Joints = new List<MD5Joint>();
            List<MD5Frame> Frames = new List<MD5Frame>();
        }

        public class MD5Model
        {
            public List<MD5MeshPart> MeshParts = new List<MD5MeshPart>();
            public List<MD5Joint> Joints = new List<MD5Joint>();
            public List<MD5Animation> Animations = new List<MD5Animation>();

            public StaticMeshPart CreateStaticMeshPart(MD5MeshPart md5_mesh_part)
            {
                StaticMeshPart mesh_part = new StaticMeshPart();

                List<Vertex> vertices = new List<Vertex>();

                foreach (MD5Vertex md5_vertex in md5_mesh_part.Vertices)
                {
                    Vertex vertex = new Vertex();

                    vertex.TextureCoordinate0 = md5_vertex.UV;
                    vertex.Position = new Vector3(0.0f);

                    float[] blend_weights = new float[4];
                    float[] blend_indices = new float[4];

                    Vector3 position = new Vector3(0.0f);

                    for (int i = 0; i < md5_vertex.WeightCount; ++i)
                    {
                        position += Vector3.Transform(md5_mesh_part.Weights[md5_vertex.WeightStartIndex + i].Position, GetMatrix(Joints[md5_mesh_part.Weights[md5_vertex.WeightStartIndex + i].JointIndex])) * md5_mesh_part.Weights[md5_vertex.WeightStartIndex + i].Bias;                           
                    }
                    
                    vertex.Position.X = position.X;
                    vertex.Position.Z = position.Y;
                    vertex.Position.Y = position.Z;

                    vertices.Add(vertex);
                }

                List<int> indices = new List<int>();
                foreach (MD5Triangle md5_triangle in md5_mesh_part.Triangles)
                {
                    indices.Add(md5_triangle.VertexIndex[2]);
                    indices.Add(md5_triangle.VertexIndex[1]);
                    indices.Add(md5_triangle.VertexIndex[0]);

                    Vertex vertex0 = vertices[md5_triangle.VertexIndex[2]];
                    Vertex vertex1 = vertices[md5_triangle.VertexIndex[1]];
                    Vertex vertex2 = vertices[md5_triangle.VertexIndex[0]];

                    CalculateNormal(ref vertex0, ref vertex1, ref vertex2);

                    vertices[md5_triangle.VertexIndex[2]] = vertex0;
                    vertices[md5_triangle.VertexIndex[1]] = vertex1;
                    vertices[md5_triangle.VertexIndex[0]] = vertex2;
                }

                for (int i = 0; i < vertices.Count; ++i)
                {
                    Vertex vertex = vertices[i];
                    vertex.Normal = Vector3.Normalize(vertex.Normal);
                    vertex.Binormal = Vector3.Normalize(vertex.Binormal);
                    vertex.Tangent = Vector3.Normalize(vertex.Tangent);
                    vertices[i] = vertex;
                }

                MeshBuffer mesh_buffer = new MeshBuffer();
                mesh_buffer.SetVertices(vertices.ToArray());
                mesh_buffer.SetIndices(indices.ToArray());
                mesh_part.SetMeshBuffer(mesh_buffer);

                return mesh_part;
            }


            protected void CalculateNormal(ref Vertex vertex0, ref Vertex vertex1, ref Vertex vertex2)
            {

                Vector3 tangent = Vector3.Normalize(((vertex2.TextureCoordinate0.Y - vertex0.TextureCoordinate0.Y) * (vertex1.Position - vertex0.Position) - (vertex1.TextureCoordinate0.Y - vertex0.TextureCoordinate0.Y) * (vertex2.Position - vertex0.Position)) / ((vertex1.TextureCoordinate0.X - vertex0.TextureCoordinate0.X) * (vertex2.TextureCoordinate0.Y - vertex0.TextureCoordinate0.Y) - (vertex1.TextureCoordinate0.Y - vertex0.TextureCoordinate0.Y) * (vertex2.TextureCoordinate0.X - vertex0.TextureCoordinate0.X)));
                Vector3 binormal = Vector3.Normalize(((vertex2.TextureCoordinate0.X - vertex0.TextureCoordinate0.X) * (vertex1.Position - vertex0.Position) - (vertex1.TextureCoordinate0.X - vertex0.TextureCoordinate0.X) * (vertex2.Position - vertex0.Position)) / ((vertex1.TextureCoordinate0.Y - vertex0.TextureCoordinate0.Y) * (vertex2.TextureCoordinate0.X - vertex0.TextureCoordinate0.X) - (vertex1.TextureCoordinate0.X - vertex0.TextureCoordinate0.X) * (vertex2.TextureCoordinate0.Y - vertex0.TextureCoordinate0.Y)));
                Vector3 normal = Vector3.Normalize(Vector3.Cross(vertex2.Position - vertex0.Position, vertex1.Position - vertex0.Position));


                vertex0.Normal += normal;
                vertex1.Normal += normal;
                vertex2.Normal += normal;

                vertex0.Tangent += tangent;
                vertex1.Tangent += tangent;
                vertex2.Tangent += tangent;

                vertex0.Binormal += binormal;
                vertex1.Binormal += binormal;
                vertex2.Binormal += binormal;
            }

            public Matrix[] CreateBones()
            {
                List<Matrix> bones = new List<Matrix>();

                foreach (MD5Joint md5_joint in Joints)
                {
                    Matrix matrix = GetMatrix(md5_joint);

                    bones.Add(matrix);                
                }

                return bones.ToArray();
            }

            protected Matrix GetMatrix(MD5Joint md5_joint)
            {
                if (md5_joint == null)
                    return Matrix.Identity;

                return Matrix.CreateFromQuaternion(md5_joint.Rotation) * Matrix.CreateTranslation(md5_joint.Translation);
            }

            // TODO : create hierarchy

        }

        public List<String> GetSupportFormats()
        {
            List<String> support_formats = new List<String>();
            support_formats.Add(".MD5");
            return support_formats;
        }

        public ResourceType GetResourceType()
        {
            return ResourceType.ANIMATION_MESH;
        }

        

        protected MD5Animation LoadMD5Animation(String filename)
        {
            try
            {
                MD5Parser md5_parser = new MD5Parser();
                MD5Animation md5_animation = new MD5Animation();

                md5_parser.LoadMD5(filename);
                md5_parser.SeekToNextKey("MD5Version");
                if (md5_parser.ReadInt() != 10)
                    return null;

                md5_parser.SeekToNextKey("numFrames");
                int frame_count = md5_parser.ReadInt();

                md5_parser.SeekToNextKey("numJoints");
                int joint_count = md5_parser.ReadInt();

                md5_parser.SeekToNextKey("frameRate");
                int frame_rate = md5_parser.ReadInt();

                md5_parser.SeekToNextKey("numAnimatedComponents");
                int animated_component_count = md5_parser.ReadInt();



                return md5_animation;
            }
            catch
            {
                return null;
            }
        }

        protected MD5Model LoadMD5Model(String path)
        {
            String filename = System.IO.Path.GetFileName(path);

            MD5Parser md5_parser = new MD5Parser();

            try
            {
                md5_parser.LoadMD5(System.IO.Path.Combine(path, filename + ".md5mesh"));
                md5_parser.SeekToNextKey("MD5Version");
                if (md5_parser.ReadInt() != 10)
                    return null;

                MD5Model md5_model = new MD5Model();
                md5_parser.SeekToNextKey("numJoints");
                int joint_count = md5_parser.ReadInt();
                md5_parser.SeekToNextKey("numMeshes");
                int mesh_count = md5_parser.ReadInt();

                md5_parser.SeekToNextKey("joints");
                md5_parser.SeekToNextKey("{");

                for (int i = 0; i < joint_count; ++i)
                {
                    MD5Joint md5_joint = new MD5Joint();
                    md5_joint.Name = md5_parser.ReadString();
                    int parent = md5_parser.ReadInt();
                    md5_joint.Parent = parent == -1 ? null : md5_model.Joints[parent];
                    md5_joint.Translation = md5_parser.ReadVector3();
                    Vector3 rotation = md5_parser.ReadVector3();

                    float w = 1.0f - (rotation.X * rotation.X) - (rotation.Y * rotation.Y) - (rotation.Z * rotation.Z);
                    w = w < 0.0f ? 0.0f : (float)-Math.Sqrt(w);
                    md5_joint.Rotation = new Quaternion(rotation, w);
                    md5_model.Joints.Add(md5_joint);
                }

                for (int i = 0; i < mesh_count; ++i)
                {
                    md5_parser.SeekToNextKey("mesh");
                    md5_parser.SeekToNextKey("{");
                    md5_parser.SeekToNextKey("numverts");

                    MD5MeshPart md5_mesh_part = new MD5MeshPart();

                    int vertex_count = md5_parser.ReadInt();

                    for (int j = 0; j < vertex_count; ++j)
                    {
                        md5_parser.SeekToNextKey("vert");
                        MD5Vertex md5_vertex = new MD5Vertex();
                        // read index
                        md5_parser.ReadInt();
                        md5_vertex.UV = md5_parser.ReadVector2();
                        md5_vertex.WeightStartIndex = md5_parser.ReadInt();
                        md5_vertex.WeightCount = md5_parser.ReadInt();
                        md5_mesh_part.Vertices.Add(md5_vertex);
                    }

                    md5_parser.SeekToNextKey("numtris");
                    int triangle_count = md5_parser.ReadInt();

                    for (int j = 0; j < triangle_count; ++j)
                    {
                        md5_parser.SeekToNextKey("tri");
                        MD5Triangle md5_triangle = new MD5Triangle();
                        // read index
                        md5_parser.ReadInt();
                        md5_triangle.VertexIndex[0] = md5_parser.ReadInt();
                        md5_triangle.VertexIndex[1] = md5_parser.ReadInt();
                        md5_triangle.VertexIndex[2] = md5_parser.ReadInt();
                        md5_mesh_part.Triangles.Add(md5_triangle);
                    }

                    md5_parser.SeekToNextKey("numweights");
                    int weight_count = md5_parser.ReadInt();

                    for (int j = 0; j < weight_count; ++j)
                    {
                        md5_parser.SeekToNextKey("weight");
                        MD5Weight md5_weight = new MD5Weight();
                        // read index
                        md5_parser.ReadInt();
                        md5_weight.JointIndex = md5_parser.ReadInt();

                        md5_weight.Bias = md5_parser.ReadFloat();
                        md5_weight.Position = md5_parser.ReadVector3();
                        md5_mesh_part.Weights.Add(md5_weight);
                    }

                    md5_model.MeshParts.Add(md5_mesh_part);
                }

                String[] animation_files = System.IO.Directory.GetFiles(path, "*.md5anim");

                foreach (String animation_file in animation_files)
                {
                    MD5Animation md5_animation = LoadMD5Animation(animation_file);

                    if (md5_animation != null)
                        md5_model.Animations.Add(md5_animation);
                }

                return md5_model;
            }
            catch
            {
                return null;
            }
        }

        public Object LoadResource(String path)
        {
            path = System.IO.Path.Combine("Content", Helper.GetFullPathWithoutExtension(path));
            String filename = System.IO.Path.GetFileName(path);

            try
            {
                MD5Model md5_model = LoadMD5Model(path);

                AnimationMesh mesh = new AnimationMesh();
                mesh.AnimationType = AnimationType.BONE;
                mesh.Bones = new Matrix[RenderSystem.MAX_BONE_COUNT];
 
                Array.Copy(md5_model.CreateBones(), mesh.Bones, Math.Min(md5_model.Joints.Count, RenderSystem.MAX_BONE_COUNT));

                MRTNormalMapMaterial material = new MRTNormalMapMaterial();
                
                material.DiffuseTexture.Create(filename + ".IMAGE2D", false);
                material.NormalTexture.Create(filename + "_normal.IMAGE2D", false);
                

                foreach (MD5MeshPart md5_mesh_part in md5_model.MeshParts)
                {
                    StaticMeshPart mesh_part = md5_model.CreateStaticMeshPart(md5_mesh_part);
                    mesh_part.Material = material.Clone();
                    mesh.AddMeshPart(mesh_part);
                }

                return mesh;
            }
            catch
            {
                return null;
            }
        }
    }
}
